{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "\n",
    "### <center> Автор материала: Мороз Денис Анатольевич (@denismoroz)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>\n",
    "### <center>Отгадай писателя ужастиков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный проект базируется на соревновании Kaggle по угадыванию вероятностей принадлежности фрагмента текста одному из трех писателей рассказов ужасов https://www.kaggle.com/c/spooky-author-identification. \n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Даны размеченные данные с тремя признаками: \n",
    "id - номер фрагмента текста; \n",
    "text - фрагмент текста\n",
    "author - автор фрагмента (целевая переменная)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import re\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.metrics import accuracy_score\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "from tqdm import tqdm\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Подтянем базы данных и ознакомимся с их структурой."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_texts = pd.read_csv('../../data/spooky_writer_train.csv')\n",
    "test = pd.read_csv('../../data/spooky_writer_test.csv')\n",
    "sample_sub= pd.read_csv('../../data/spooky_writer_sample_submission.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_texts.info(())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test.info()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_texts.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_sub.info()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_sub.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ознакомимся с данными. Построим на графике количество фрагментов текста по каждому автору в тестовой выборке. В целевой переменной у нас всего три автора и фрагменты распределены плюс минус равномерно. Как видим, это задача мультиклассовой классификации с тремя целевыми признаками."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_total = len(train_texts)\n",
    "num_eap = len(train_texts.loc[train_texts.author == 'EAP'])\n",
    "num_hpl = len(train_texts.loc[train_texts.author == 'HPL'])\n",
    "num_mws = len(train_texts.loc[train_texts.author == 'MWS'])\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "eap, hpl, mws = plt.bar(np.arange(1, 4), [(num_eap/num_total)*100, (num_hpl/num_total)*100, (num_mws/num_total)*100])\n",
    "ax.set_xticks(np.arange(1, 4))\n",
    "ax.set_xticklabels(['Edgar Allan Poe (EAP)', 'H.P. Lovecraft (HPL)', 'Mary Shelley (MWS)'])\n",
    "ax.set_ylim([0, 60])\n",
    "ax.set_ylabel('% фрагментов', fontsize=12)\n",
    "ax.set_xlabel('Имя автора', fontsize=12)\n",
    "ax.set_title('Распределение фрагментов')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вполне очевидно, что каждого автора отличает определенный лексический запас и на этом в первую очередь необходимо базироваться при построении обучающейся модели. Напишем функцию, которая выводит \n",
    "матрицу общих пар слов больше трех символов (или комбинации из двух слов) для пар авторов.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def common_words_matrix(df):  \n",
    "    columns=[\"words\",\"author\"]\n",
    "    words_pool=pd.DataFrame(columns=columns)\n",
    "    for t in tqdm(range(len(df))):\n",
    "        words=re.findall('\\w{3,}', df[\"text\"].iloc[t].lower())\n",
    "        words = [' '.join(ws) for ws in zip(words, words[1:])]\n",
    "        \n",
    "        for word in words:\n",
    "            words_pool.loc[words_pool.shape[0]]=[word,df[\"author\"].iloc[t]]\n",
    "    \n",
    "    cross=pd.crosstab(words_pool.words,words_pool.author)\n",
    "    columns_m=[\"EAP\",\"HPL\",\"MWS\"]\n",
    "    index_m=[\"EAP\",\"HPL\",\"MWS\"]\n",
    "    matrix=pd.DataFrame(columns=columns_m,index=index_m)\n",
    "    matrix.loc[\"EAP\",\"EAP\"]=len(cross.loc[cross.EAP==1])\n",
    "    matrix.loc[\"HPL\",\"HPL\"]=len(cross.loc[cross.HPL==1])\n",
    "    matrix.loc[\"MWS\",\"MWS\"]=len(cross.loc[cross.MWS==1])\n",
    "    matrix.loc[\"EAP\",\"HPL\"]=len(cross.loc[(cross.EAP==1) & (cross.HPL==1)])\n",
    "    matrix.loc[\"HPL\",\"EAP\"]=len(cross.loc[(cross.EAP==1) & (cross.HPL==1)])\n",
    "    matrix.loc[\"EAP\",\"MWS\"]=len(cross.loc[(cross.EAP==1) & (cross.MWS==1)])\n",
    "    matrix.loc[\"MWS\",\"EAP\"]=len(cross.loc[(cross.EAP==1) & (cross.MWS==1)])\n",
    "    matrix.loc[\"HPL\",\"MWS\"]=len(cross.loc[(cross.HPL==1) & (cross.MWS==1)])\n",
    "    matrix.loc[\"MWS\",\"HPL\"]=len(cross.loc[(cross.HPL==1) & (cross.MWS==1)])\n",
    "    print(\"Количество общих слов (комбинаций слов)\")\n",
    "    print(matrix)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Запустим функцию common_words_matrix, чтобы увидеть сколько общих пар слов, которые \n",
    "идут подряд, используют авторы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "common_words_matrix(train_texts)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из полученных матриц мы видим, что количество общих пар слов не так уж велико, это может характеризовать некую уникальность каждого автора и на этом можно базироваться, подбирая признаки для модели. Комбинации из трех и более пар слов считаю использовать нецелесообразно, т.к. модель может переобучиться. Фрагменты текстов относительно коротки и априори маловероятно, что те же тройки слов, которые встречались в тренировочной выборке станут попадаться в тестовых выборках."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для решения данной задачи классификации я решил использовать инструмент Vowpal Wabbit (VW), который имеет следующие преимущества:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- хорошая скорость обучение модели и прогнозирования; \n",
    "\n",
    "- возможность использования нелинейных признаков (посредством ngram);\n",
    "\n",
    "- удобная настройка параметров;\n",
    "\n",
    "- встроенная кросс-валидация."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Самое главное достоинство - это удобное разбиение признакового пространства слов на пары - свойства, которое я собираюсь использовать для обучения модели и предстказания."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Закодируем буквенные символы авторов с помощью цифр 1,2,3, т.к. многоклассовая классификация VW принимает на вход только цифры."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d = {\"EAP\":1,\"MWS\":2,\"HPL\":3}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_texts[\"author_code\"]=train_texts[\"author\"].map(d)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_texts.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Напишем функцию для записи таблиц в формат VWю"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_vw_format(out_vw,df,is_train=True):\n",
    "    with open(out_vw,\"w\") as out:\n",
    "        for i in range(df.shape[0]):\n",
    "           \n",
    "            if is_train:\n",
    "                target = df[\"author_code\"].iloc[i]\n",
    "            else:\n",
    "                target = 1 # в тестовой выборке target может быть любым\n",
    "            text = df[\"text\"].iloc[i].replace(\"\\n\",\"\").replace(\"|\",\"\").replace(\":\",\"\").lower() #удалим спецсимволы\n",
    "            text = \" \".join(re.findall(\"\\w{3,}\",text)) #оставим слова более 2 символов\n",
    "            s = \"{} |text {}\\n\".format(target,text)\n",
    "            out.write(s)   \n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разобьем выборку на обучающую и тестовую, % разбиения - по умолчанию."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train, valid = train_test_split(train_texts,random_state=13)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим размеры выборок"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(train.shape[0],test.shape[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Запишем преобразованные выборки в файлы. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_vw_format(\"train.vw\",train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!head -2 train.vw"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_vw_format(\"valid.vw\",valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!head -2 valid.vw"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_vw_format(\"test.vw\",test,is_train=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!head -2 test.vw"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Запустим Vowpal Wabbit на сформированном файле. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!rm train.vw.cache"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!vw --oaa 3 train.vw -f model.vw -b 24 --random_seed 17 --loss_function logistic --ngram 2 --passes 30 \\\n",
    "--learning_rate 0.5 --power_t 0.5 -k -c -q ff"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим на валидационной выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "!vw -i model.vw -t -d valid.vw -p valid_pred.txt --random_seed 17"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В задании на Kaggle точность оценивается с помощью метрики logloss. В связи с эти в модели VW была настроена логистическая функция потерь. Кросс-валидации на тренировочной выборке дала хороший результат; average loss = 0.0139812, на валидационной выборке - 0.155669. \n",
    "\n",
    "Также метрикой для оценки модели на валидационной выборке может служить accuracy_score, которая широко используется на практике для оценки задач классификации. В данном случае, как видно ниже, она показывает неплохой результат для такой простой модели, как наша."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open('valid_pred.txt') as pred_file:\n",
    "    valid_pred = [float(label) for label in pred_file.readlines()]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy_score(valid[\"author_code\"], valid_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вручную я проводил изменения параметров VW, таких как количество проходов, количество ngram, регуляризация и пр., однако они дают худший результат."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Запустим на тестовой выборке и сформируем посылку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "!vw -i model.vw -t -d test.vw -p test_pred.csv --random_seed 17 -r test_prob.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_prob=pd.read_csv(\"test_prob.txt\",header=None,sep=\" |:\",names=[\"x\",\"EAP\",\"x\",\"MWS\",\"x\",\"HPL\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_prob.head()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Используя формулу  1/(1+exp(-score)) преобразуем полученные значения вероятностей."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in [\"EAP\",\"MWS\",\"HPL\"]:\n",
    "    test_prob[i] = 1/(1+np.exp(-test_prob[i])) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_prob.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сверим размеры, полученного файла с прогнозами, и формы для посылки, скачанного с Kaggle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(test_prob.shape)\n",
    "print(sample_sub.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_sub.EAP=test_prob[\"EAP\"]\n",
    "sample_sub.HPL=test_prob[\"HPL\"]\n",
    "sample_sub.MWS=test_prob[\"MWS\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_sub.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_sub.to_csv('benchmark_submission.csv',index=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bench=pd.read_csv(\"benchmark_submission.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bench.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посылку отправили на Kaggle. С первого раза 209 место из 519. Думаю, неплохо!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/spooky_writer_Screenshot.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Вывод: VW c минимальным количеством признаком дал неплохой результат, который может служить базой для дальнейших улучшений. В первую очередь необходимо добавить количество признаков, напр., количество знаков пунктуации, соотношение позитивных/негативных слов и др. Также улучшить результат позволит использование других моделей NLP, таких как word2vec."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
